/**
 * @fileoverview Typegraph visualizer for pytype.
 *
 * This file is intended to be injected into the Jinja2 template for the
 * visualizer, in order to keep that as a single file. It's a separate file to
 * take advantage of JS tools and IDE integration.
 *
 * IDs are very important in this. There are two kinds:
 * - Serialized ID, the ID number for a part of the pytype typegraph.
 * - Cytoscape ID, the value of the data.id field for an element in the graph.
 * Serialized IDs are read-only and are not generated by the visualizer.
 * Cytoscape IDs are derived from Serialized IDs, though SourceSet IDs are a bit
 * more complex than that.
 */

/**
 * Visualizer handles manipulating the Cytoscape instance using the pytype
 * program data.
 * `cy` is a Cytoscape object, created by e.g. `cy = cytoscape({...});`.
 * `program` is an object created by
 * pytype.typegraph.typegraph_serializer.TypegraphEncoder, generated from a
 * pytype.typegraph.cfg.Program.
 */
class Visualizer {
  /**
   * Construct a Visualizer instance.
   * @param {!Object} cy A Cytoscape object.
   * @param {!Object} code A serialized pytype Program, generated by
   * pytype.typegraph.typegraph_serializer.TypegraphEncoder.
   */
  constructor(cy, code) {
    /** @private @const {!Object} */
    this.cy = cy;

    /** @private @const {!Object} */
    this.code = code;

    let block_nodes = this.code.blocks.map(n => this.gen_block(n));
    let block_edges = this.code.blocks.flatMap(
        n => n.outgoing.map(
            o_id => this.gen_edge(
                'block_edge', this.block_id(n.id), this.block_id(o_id))));
    this.cy.batch(() => {
      this.cy.add(block_nodes);
      this.cy.add(block_edges);
    });
    this.relayout();
  }

  /**
   * Run the defined layout. This is required after adding nodes.
   */
  relayout() {
    this.cy.layout(this.cy.data('layout_options')).run();
  }

  /**
   * Returns the Cytoscape ID for the given Block ID.
   * Note that this operates on *IDs*, not on full blocks.
   * @param {number} block A pytype block id.
   * @return {string} The Cytoscape ID for this block.
   */
  block_id(block) {
    return `block_${block}`;
  }

  /**
   * Generate a Cytoscape node for the given Block.
   * @param {!Object} block A SerializedBlock object
   * @return {!Object} A Cytoscape element representing the Block
   */
  gen_block(block) {
    return {
      group: 'nodes',
      data: {
        id: this.block_id(block.id),
        name: `tbn_${block.name}`,
        code: block.code,
        class: 'block_node',
      },
      classes: ['block_node'],
    };
  }

  /**
   * Generic edge generator.
   * @param {string} kind The kind of edge, e.g. cfg_node_edge indicates an edge
   * between two Blocks, while var_bind_edge indicates an edge between a
   * Variable and a Binding. This should (but not must) match one of the edge
   * styles defined in this.cy.style.
   * @param {string} source The Cytoscape ID of the source.
   * @param {string} target The Cytoscape ID of the target.
   * @return {!Object} a Cytoscape element representing the edge.
   */
  gen_edge(kind, source, target) {
    return {
      group: 'edges',
      data: {
        id: `${kind}_${source}_${target}`,
        source: source,
        target: target,
        class: kind,
      },
      classes: [kind],
    };
  }

  /**
   * Checks if an element with the given ID already exists in the graph.
   * @param {string} elem The Cytoscape ID to check for.
   * @return {!bool} true if an element with the given ID exists.
   */
  elem_exists(elem) {
    return this.cy.$id(elem).nonempty();
  }

  /**
   * Adds all elements in the list that aren't already in the graph to the graph.
   * Cytoscape does not like it when you add an element with the same ID as an
   * existing element.
   * @param {!Array<!Object>} elems Array of Cytoscape elements to possibly add.
   */
  add_elems(elems) {
    this.cy.add(elems.filter(e => !this.elem_exists(e.data.id)));
  }

  /**
   * Adds a Variable and its nodes to the graph, or toggles the visibility of
   * those nodes if they already exist.
   * This is intended to be used by the variable table in the visualizer.
   * @param {number} var_id The ID number of the Variable to reveal.
   */
  add_or_hide_var(var_id) {
    if (this.elem_exists(this.variable_id(var_id))) {
      this.get_var_cluster(var_id).toggleClass('hidden_node');
    } else {
      this.reveal_var(var_id);
    }
    this.get_var_cluster(var_id).flashClass('highlight_node', 1000);
  }

  /**
   * Toggles the `highlight_node` Cytoscape style class.
   * Has no effect if the node for the Variable does not exist.
   * This is intended to be used by the variable table in the visualizer.
   * There are two functions for this so it does not interfere with the
   * highlight from add_or_hide_var.
   * @param {number} var_id The ID number of the Variable to highlight.
   */
  highlight_var(var_id) {
    if (this.elem_exists(this.variable_id(var_id))) {
      this.get_var_cluster(var_id).toggleClass('highlight_node', true);
    }
  }

  /**
   * Toggles the `highlight_node` Cytoscape style class.
   * Has no effect if the node for the Variable does not exist.
   * This is intended to be used by the variable table in the visualizer.
   * @param {number} var_id The ID number of the Variable to highlight.
   */
  unhighlight_var(var_id) {
    if (this.elem_exists(this.variable_id(var_id))) {
      this.get_var_cluster(var_id).toggleClass('highlight_node', false);
    }
  }

  /**
   * Generates the Cytoscape elements for the legend.
   * These should be used in another Cytoscape instance.
   * @return {!Array<!Object>} List of elements usable by Cytoscape.
   */
  gen_legend() {
    return [];
  }
}
